﻿namespace SpikingAnalyticsLib.ProbabilityModel

open System.Collections.Generic
open System.IO
open SpikingNeuronLib
open SpikingAnalyticsLib
open SpikingAnalyticsLib.GeneralUtilities
open SpikingAnalyticsLib.PatternUtilities
open SpikingAnalyticsLib.NetworkUtilities
open SpikingAnalyticsLib.ResponseFingerPrint

//===========================================================================================================
// Types for exchanging data with vanilla .Net APIs

/// Attributes that define a Temporal Window
[<NoEquality;NoComparison>]
type WindowData =
    class
        val WindowStartTime:int
        val WindowEndTime:int
        val FrameSpikeFrequency:float

        new (minTime, maxTime, spikeFrequency) =
            {
                WindowStartTime = minTime;
                WindowEndTime = maxTime;
                FrameSpikeFrequency = spikeFrequency;
            }
    end

/// A frame-unique Temporal Window identifier
[<NoComparison>]
type WindowID =
    class
        val NeuronIndex:int
        val PatternIndex:int

        new (neuronIndex, patternIndex) =
            {
                NeuronIndex = neuronIndex
                PatternIndex = patternIndex
            }
    end

//===========================================================================================================
// MG Deprecated

type NetworkEngine(networkSpecifier:CrossbarNetworkSpecifier, backgroundFiringRate, verbose) = class

    let SetDataCollectionEnabled (spn:CrossbarNetwork) firingEventCollectionHandler enabled =
        if enabled then
            spn.add_OneSecondTickBeforeCleanup(firingEventCollectionHandler)
        else
            spn.remove_OneSecondTickBeforeCleanup(firingEventCollectionHandler)

    let CreateNetwork (millisecTickCollector:OneMillisecTickDataCollector option) (networkSpecifier:CrossbarNetworkSpecifier) verbose =

        let _spn = new CrossbarNetwork(networkSpecifier)

        _spn.AddFixedConnectionList(int (float networkSpecifier.TotalNeurons * 0.1), networkSpecifier.DefaultExcitatoryWeight, networkSpecifier.DefaultInhibitoryWeight)

        _spn.add_RunWarning(new RunWarningEventHandler(fun msg -> printfn "%s" msg))

        if millisecTickCollector.IsSome then
            let millisecondHandler = millisecTickCollector.Value.MillisecTickHandler (upcast _spn)       // capture the network reference
            _spn.add_OneMillisecondTick(new OneMillisecondTickEventHandler(millisecondHandler))

        _spn

    let spn = CreateNetwork None networkSpecifier verbose

    let firingEventCollector = new OneSecondTickDataCollector()
    let firingEventCollectionHandler =
        let oneSecTickHandler = firingEventCollector.SecondTickHandler (upcast spn) verbose   // capture the network reference
        new OneSecondTickBeforeCleanupEventHandler(oneSecTickHandler)

    let patternGenerator = Some(new PoissonProcessPatternGenerator(networkSpecifier.TotalNeurons, backgroundFiringRate))

    let EnableDataCollection () =
        // disable learning
        spn.PlasticityDisabled <- true

        firingEventCollector.Clear()
        SetDataCollectionEnabled spn firingEventCollectionHandler true

    let DisableDataCollection () =
        // re-enable learning
        spn.PlasticityDisabled <- false

        SetDataCollectionEnabled spn firingEventCollectionHandler false

    member internal this.NetworkEngine = spn

    /// The total number of excitatory and inhibitory neurons in the network
    member this.TotalNeurons = spn.TotalNeurons

    /// The number of excitatory neurons in the network
    member this.NumExcitatoryNeurons = spn.ExcitatoryNeurons

    /// The number of inhibitory neurons in the network
    member this.NumInhibitoryNeurons = spn.InhibitoryNeurons

    /// The maximum axonal delay in milliseconds
    member this.MaxDelay = spn.MaximumAxonalDelay

    /// Set whether synaptic plasticity is enabled or disabled
    member this.SetPlasticityDisabled disabled = spn.PlasticityDisabled <- disabled

    /// Return a Dictionary of afferent connections for each neuron
    member this.GetIncomingConnections () =
        ConvertMappedValues ConvertListToEnumerable this.IncomingConnections
        |> ConvertMapToDictionary 

    /// Return a Dictionary of efferent connections for each neuron
    member this.GetOutgoingConnections () =
        ConvertMappedValues ConvertListToEnumerable this.OutgoingConnections
        |> ConvertMapToDictionary 

    /// Get the accumulated firing events from the last run
    member this.GetFiringEvents () =
        this.FiringEventList
        |> Seq.map (fun (time, nindex, isBackgroundEvent) -> new FiringEvent(time, nindex, isBackgroundEvent))

    member internal this.IncomingConnections =
        GetConnectionsByNeuron spn (fun connection -> connection.PostNeuron)

    member internal this.OutgoingConnections =
        GetConnectionsByNeuron spn (fun connection -> connection.PreNeuron)

    member internal this.FiringEventList = firingEventCollector.FiringEvents

    /// Save the network state to the specified path
    member this.SaveNetwork (path:string) =
        spn.SaveNetworkState(path)

    /// Load the network state from the specified path
    member this.LoadNetwork (path:string) =
        spn.LoadNetworkState(path)

    /// Run the network with the specified pattern and learning disabled whilst accumulating firing events
    member this.RunWithPattern(inputPattern:IStimulus, runSeconds) =
        let pattern =
            if inputPattern = null then
                None
            else
                Some(inputPattern)
        this.RunWithPattern(pattern, runSeconds)

    /// Run the network with the specified pattern and learning disabled whilst accumulating firing events
    member internal this.RunWithPattern(inputPattern, runSeconds) =
        EnableDataCollection ()
        RunNetwork spn runSeconds patternGenerator inputPattern
        DisableDataCollection ()

    /// Generate spike count profiles for each neuron and pattern
    member internal this.GenerateFrameProfiles (firingPatternMap:Map<int, (int * int) list option>) inputPeriod runSeconds verbose =
        if verbose then printfn "Computing core statistics..."

        // profile only excitatory neurons for each pattern
        // inhibitory neurons tend to be less pattern-specific
        let excitatoryNeurons = [for i in 0..this.NumExcitatoryNeurons-1 -> i]

        let inputPatterns =
            firingPatternMap
            |> Map.map (fun index pattern ->
                    if pattern.IsSome then
                        Some(Stimulus.Create(new Pattern(pattern.Value), inputPeriod) :> IStimulus)
                    else
                        None
                )

        if verbose then printfn "\tSelecting windows..."
        let frameProfilesByPattern =
            inputPatterns
            |> Map.map (fun index pattern ->
                    // gather firing data
                    if verbose then printf "\t\tcollecting data..."
                    this.RunWithPattern(pattern, runSeconds)
                    let firingEvents = this.FiringEventList
                    if verbose then printf "analysing %d events..." firingEvents.Count

                    // create frame profiles for all neurons
                    let frameProfiles = FrameProfile.GenerateFrameProfileData(inputPeriod, this.TotalNeurons, excitatoryNeurons, firingEvents)
                    if verbose then printfn "done"
                    frameProfiles
                )

        frameProfilesByPattern

    member internal this.CollectFixedFrameTestRunData(runSeconds, inputPatterns) =

        // generate some test data: randomly select an input pattern and record the selection against the frame number
        // input patterns have an input period of 1000 msec for convenience in matching with data collection
        printf "Collecting test data over %d seconds" runSeconds
        EnableDataCollection ()
        let selectedPatternBySecond, collectedData = CollectFixedFrameDataRandomSelection spn firingEventCollector.FiringEvents runSeconds patternGenerator inputPatterns
        DisableDataCollection ()
        printfn "done"

        selectedPatternBySecond, collectedData

    member internal this.CollectFrameFreeTestRunData runSeconds inputPatterns =

        // generate some test data: randomly select an input pattern and record the selection against the current second
        // input patterns have an input period of 1000 msec for convenience in matching with data collection
        printf "Collecting test data over %d seconds" runSeconds
        EnableDataCollection ()
        let selectedPatternBySecond, collectedData = CollectFrameFreeRunData spn firingEventCollector.FiringEvents runSeconds patternGenerator inputPatterns
        DisableDataCollection ()
        printfn "done"

        selectedPatternBySecond, collectedData

    member internal this.LearnPatternsRandomly runSeconds secsPerPattern (inputPatterns:Map<int, IStimulus option>) =
        DisableDataCollection ()

        let r = System.Random()
        let numPatterns = inputPatterns.Count
        let numSelections = runSeconds / secsPerPattern

        printfn "Learning %d patterns (%d secs / pattern; %d secs total)" numPatterns secsPerPattern runSeconds
        printf "Iterating (%d iterations) " numSelections
        let patternSelectionMap =
            Seq.init numSelections (fun section ->
                    let selectedIndex = r.Next(numPatterns)
                    let inputPattern = inputPatterns.[selectedIndex]

                    if inputPattern.IsSome then
                        spn.Run(secsPerPattern, patternGenerator.Value, inputPattern.Value)
                    else
                        spn.Run(secsPerPattern, patternGenerator.Value)

                    printf "."
                    section, selectedIndex
                )
            |> Map.ofSeq
        printfn " done"
        patternSelectionMap

    /// Learn the specified input patterns (firing events are not accumulated)
    member this.LearnPatternsSequentially(secsPerPattern, inputPatterns:IDictionary<int, IStimulus>) =
        this.LearnPatternsSequentially(secsPerPattern, (ConvertToPatternMap inputPatterns))

    member internal this.LearnPatternsSequentially(secsPerPattern, inputPatterns:Map<int, IStimulus option>) =
        DisableDataCollection ()

        let numPatterns = inputPatterns.Count
        let runSeconds = numPatterns * secsPerPattern

        printfn "Learning %d patterns (%d secs / pattern; %d secs total)" numPatterns secsPerPattern runSeconds
        printf "Iterating (%d iterations) " numPatterns
        for pairs in inputPatterns do
                let inputPattern = pairs.Value

                if inputPattern.IsSome then
                    spn.Run(secsPerPattern, patternGenerator.Value, inputPattern.Value)
                else
                    spn.Run(secsPerPattern, patternGenerator.Value)

                printf "."

        printfn " done"

    /// Find patterns that the network can learn using the congruent method
    member this.FindCongruentPatterns(numPatterns) =

        let inputPatterns = FindCongruentPatterns numPatterns spn
        PatternList(inputPatterns)

    /// Use the underlying network connectivity to generate a structural description of the specified firing events
    // i.e. link events that are causally related
    member this.GetPNGDescriptor(groupFiringEvents:seq<FiringEvent>) =
        this.GetPNGDescriptor(groupFiringEvents, 0.0)

    member this.GetPNGDescriptor(groupFiringEvents:seq<FiringEvent>, synapticThreshold) =

        let outgoingConnections = GetConnectionsByNeuron spn (fun connection -> connection.PreNeuron)
        let groupEvents = Seq.map (fun (evt:FiringEvent) -> evt.Time, evt.NeuronIndex, 1.0) groupFiringEvents   // default link probability of 1.0
        let jitter = 2  // was spn.MaximumAxonalDelay
        let minTime, maxTime, linkedEventsList = GetGroupLinksFromPostConnectionMap jitter synapticThreshold outgoingConnections groupEvents
        PNGDescriptor(minTime, maxTime, linkedEventsList)
end

//===========================================================================================================
/// PatternStatistics: a type that holds statistical data on specified patterns

// frameProfilesByPattern:
//  outer: key = pattern index; value = frameProfiles for pattern
//  middle: key = neuron index; value = frameProfile for neuron
//  inner: key = time offset; value = spike count
[<AllowNullLiteral>]
type PatternStatistics private (windowSize, inputPeriod, numFrames, initialThreshold, frameFrequencyThreshold, firingPatternMap:Map<int, (int * int) list option>, frameProfilesByPattern:Map<int, Map<int, Map<int, int>>>) =

    let ConvertToExternalMap (windowMap:Map<int, Map<int, (int * int * float)>>) =
        windowMap
        |> Map.fold (fun (outerState:Dictionary<int, Dictionary<int, WindowData>>) outerKey innerMap ->
                let innerDictionary =
                    innerMap
                    |> Map.fold (fun (innerState:Dictionary<int, WindowData>) innerKey (minTime, maxTime, spikeFrequency) ->
                            let windowData = new WindowData(minTime, maxTime, spikeFrequency)
                            innerState.Add(innerKey, windowData)
                            innerState
                        ) (new Dictionary<int, WindowData>(innerMap.Count))
                outerState.Add(outerKey, innerDictionary)
                outerState
            ) (new Dictionary<int, Dictionary<int, WindowData>>(windowMap.Count))

    let ConvertToExternalSpikeFrequencyMap (spikeFrequencyMap:Map<int, Map<(int * int), float>>) =
        spikeFrequencyMap
        |> Map.fold (fun (outerState:Dictionary<int, Dictionary<WindowID, float>>) outerKey innerMap ->
                let innerDictionary =
                    innerMap
                    |> Map.fold (fun (innerState:Dictionary<WindowID, float>) (neuronIndex, patternIndex) frequency ->
                            let windowID = new WindowID(neuronIndex, patternIndex)
                            innerState.Add(windowID, frequency)
                            innerState
                        ) (new Dictionary<WindowID, float>(innerMap.Count))
                outerState.Add(outerKey, innerDictionary)
                outerState
            ) (new Dictionary<int, Dictionary<WindowID, float>>(spikeFrequencyMap.Count))

    let ConvertToExternalSpikeDistributionMap (spikeDistributionMap:Map<int, Map<bool, Map<(int * int), float>>>) =
        spikeDistributionMap
        |> Map.fold (fun (outerState:Dictionary<int, Dictionary<bool, Dictionary<WindowID, float>>>) outerKey middleMap ->
                let middleDictionary =
                    middleMap
                    |> Map.fold (fun (middleState:Dictionary<bool, Dictionary<WindowID, float>>) middleKey innerMap ->
                            let innerDictionary =
                                innerMap
                                |> Map.fold (fun (innerState:Dictionary<WindowID, float>) (neuronIndex, patternIndex) frequency ->
                                        let windowID = new WindowID(neuronIndex, patternIndex)
                                        innerState.Add(windowID, frequency)
                                        innerState
                                    ) (new Dictionary<WindowID, float>(innerMap.Count))
                            middleState.Add(middleKey, innerDictionary)
                            middleState
                        ) (new Dictionary<bool, Dictionary<WindowID, float>>(middleMap.Count))
                outerState.Add(outerKey, middleDictionary)
                outerState
            ) (new Dictionary<int, Dictionary<bool, Dictionary<WindowID, float>>>(spikeDistributionMap.Count))

    let CreatePatternNeuronWindowMap windowSize initialThreshold numFrames (frameProfilesByPattern:Map<int, Map<int, Map<int, int>>>) =
        let patternNeuronWindowMap =
            firingPatternMap
            |> Map.map (fun patternIndex pattern ->
                    let frameProfiles = frameProfilesByPattern.[patternIndex]

                    let excludeSet =
                        if pattern.IsSome then
                            pattern.Value
                            |> Seq.map (fun (time, nindex) -> nindex)
                            |> Set.ofSeq
                        else
                            Set.empty

                    // select windows on selected neurons
                    let windowMap = WindowMap.SelectBestWindowsOriginal(windowSize, numFrames, initialThreshold, frameFrequencyThreshold, excludeSet, frameProfiles)

                    windowMap
                )

        patternNeuronWindowMap

    let CreateNeuronPatternWindowMap windowSize initialThreshold numFrames frameProfilesByPattern =
        let InvertWindowMap patternNeuronWindowMap =
            let neuronPatternWindowMap =
                patternNeuronWindowMap
                |> Map.fold (fun (outer:Map<int, _>) patternIndex patternData ->
                        patternData
                        |> Map.fold (fun (inner:Map<int, _>) nindex windowData ->
                                let patternMap =
                                    if inner.ContainsKey nindex then
                                        inner.[nindex]
                                    else
                                        Map.empty
                                Map.add nindex (Map.add patternIndex windowData patternMap) inner
                            ) outer
                    ) Map.empty

            neuronPatternWindowMap

        let patternNeuronWindowMap = CreatePatternNeuronWindowMap windowSize initialThreshold numFrames frameProfilesByPattern

        // invert the maps so that the outer key is neuron index and the inner key is pattern
        let neuronPatternWindowMap = InvertWindowMap patternNeuronWindowMap

        neuronPatternWindowMap

    // Compute the window spike ratios for each window and each input pattern i.e. probability of window spike given pattern
    let CreatePatternSpikeFrequencyMap windowSize initialThreshold frameProfilesByPattern =

        // Given a map of selected windows (keyed by neuron/pattern), and a profile map for a particular input pattern
        // get the window spike ratios (number of spikes within the window as a proportion of the total)
        let CreateSpikeRatioMap (windowMap:Map<_, _>) profilesByNeuron =
            profilesByNeuron
            |> Map.toSeq
            |> Seq.filter (fun (nindex, _) -> windowMap.ContainsKey nindex)
            |> Seq.map (fun (nindex, profile) ->
                    let totalSpikes =
                        profile
                        |> Map.fold (fun total time count -> total + count) 0

                    // compute frequencies for each window (i.e. each combination of neuron / pattern in windowMap)
                    windowMap.[nindex]
                    |> Map.toSeq
                    |> Seq.map (fun (patternIndex, (minTime, maxTime, _)) ->
                            let windowSpikes =
                                profile
                                |> Map.toSeq
                                |> Seq.filter (fun (time, count) -> time >= minTime && time <= maxTime)
                                |> Seq.sumBy (fun (time, count) -> count)
                            (nindex, patternIndex), (float windowSpikes / float totalSpikes)
                        )
                )
            |> Seq.concat
            |> Map.ofSeq

        let neuronPatternWindowMap = CreateNeuronPatternWindowMap windowSize initialThreshold numFrames frameProfilesByPattern
        frameProfilesByPattern
        |> Map.map (fun patternIndex profile -> CreateSpikeRatioMap neuronPatternWindowMap profile)

    // Compute window spike frequencies for scoring with whole distributions
    // Compute the window spike ratios for each window and each input pattern i.e. probability of window spike given pattern
    let CreatePatternSpikeDistributionMap windowSize initialThreshold numFrames frameProfilesByPattern =

        let CombineProfiles (frameProfilesByPattern:Map<int, Map<int, Map<int, int>>>) =

            let backgroundFrameProfilesMap = Map.add false frameProfilesByPattern.[0] Map.empty

            let combinedFrameProfiles =
                frameProfilesByPattern
                |> Map.fold (fun combinedProfilesMap patternIndex profile ->
                        if patternIndex > 0 then
                            // the profiles for when the pattern is present or not present
                            let trueFalseMap = Map.add true profile backgroundFrameProfilesMap
                            Map.add patternIndex trueFalseMap combinedProfilesMap
                        else
                            combinedProfilesMap
                    ) Map.empty

            combinedFrameProfiles

        // Given a map of selected windows (keyed by pattern/neuron), and a profile map for a particular input pattern
        // get the window spike ratios (number of spikes within the window as a proportion of the total)
        // MG Note that this is recomputing the windowSpikeCountRatio from SelectWindows (see WindowMap.fs)
        let CreateSpikeRatioMap patternIndex (windowMap:Map<int, Map<int, (int * int * float)>>) profilesByNeuron =
            let neuronWindowMap = windowMap.[patternIndex]
            profilesByNeuron
            |> Map.toSeq
            |> Seq.filter (fun (nindex, _) -> neuronWindowMap.ContainsKey nindex)
            |> Seq.map (fun (nindex, profile) ->
                    let totalSpikes =
                        profile
                        |> Map.fold (fun total time count -> total + count) 0

                    let minTime, maxTime, _ = neuronWindowMap.[nindex]
                    let windowSpikes =
                        profile
                        |> Map.toSeq
                        |> Seq.filter (fun (time, count) -> time >= minTime && time <= maxTime)
                        |> Seq.sumBy (fun (time, count) -> count)
                    (nindex, patternIndex), (float windowSpikes / float totalSpikes)
                )
            |> Map.ofSeq

        let patternNeuronWindowMap = CreatePatternNeuronWindowMap windowSize initialThreshold numFrames frameProfilesByPattern
        let distributionProfilesByPattern = CombineProfiles frameProfilesByPattern

        distributionProfilesByPattern
        |> Map.map (fun patternIndex profile ->
                profile
                |> Map.map (fun patternPresent profile -> CreateSpikeRatioMap patternIndex patternNeuronWindowMap profile)
            )

    // --------------------------------------------------------------------------------------------------
    // Public Constructor

    new(engine:NetworkEngine, firingPatterns:PatternList, verbose) =

        let windowSize = 8
        let inputPeriod = 250
        let runSeconds = 100
        let numFrames = (1000 / inputPeriod) * runSeconds

        // initial threshold value (to reduce the amount of processing)
        let initialThreshold =
            let expectedFraction = float windowSize / float inputPeriod
            5.0 * expectedFraction

        // final threshold value
        let frameFrequencyThreshold = 0.75

        let firingPatternMap =
            // background-only pattern at map key 0
            let backgroundOnly = Map.add 0 None Map.empty
            firingPatterns.Patterns
            |> Seq.mapi (fun index pattern -> index + 1, pattern)
            |> Seq.fold (fun map (index, pattern) ->
                    Map.add index pattern map
                ) backgroundOnly

        let frameProfilesByPattern = engine.GenerateFrameProfiles firingPatternMap inputPeriod runSeconds verbose

        new PatternStatistics(windowSize, inputPeriod, numFrames, initialThreshold, frameFrequencyThreshold, firingPatternMap, frameProfilesByPattern)

    member this.PatternProfiles = frameProfilesByPattern

    member this.PatternNeuronWindowMap = CreatePatternNeuronWindowMap windowSize initialThreshold numFrames frameProfilesByPattern

    member this.NeuronPatternWindowMap = CreateNeuronPatternWindowMap windowSize initialThreshold numFrames frameProfilesByPattern

    member this.PatternSpikeFrequencyMap = CreatePatternSpikeFrequencyMap windowSize initialThreshold frameProfilesByPattern

    member this.PatternSpikeDistributionMap = CreatePatternSpikeDistributionMap windowSize initialThreshold numFrames frameProfilesByPattern

    member this.Save(outputFilePath) =
        use sw = File.CreateText(outputFilePath)
        let header = sprintf "%d %d %d %f %f" windowSize inputPeriod numFrames initialThreshold frameFrequencyThreshold
        sw.WriteLine(header)

        sw.WriteLine(firingPatternMap.Count)
        firingPatternMap
        |> Map.iter (fun index pattern ->
                let patternString =
                    if pattern.IsSome then
                        StringifyWithSeparator " " pattern.Value
                    else
                        "None"
                sw.WriteLine(patternString)
            )

        sw.WriteLine()
        frameProfilesByPattern
        |> Map.iter (fun patternIndex frameProfiles ->
                sw.WriteLine(patternIndex)
                frameProfiles
                |> Map.iter (fun neuronIndex neuronProfile ->
                        sw.WriteLine(neuronIndex)
                        let profileString =
                            neuronProfile
                            |> Map.toSeq
                            |> Seq.map (fun (timeOffset, spikeCount) -> spikeCount)
                            |> StringifyWithSeparator " "
                        sw.WriteLine(profileString)
                    )
                sw.WriteLine()
            )

    static member Load(inputFilePath) =
        PatternStatistics.Load(inputFilePath, false)

    static member Load(inputFilePath, verbose) =
        if verbose then
            printf "Loading %s..." (Path.GetFileName(inputFilePath))

        use sr = File.OpenText(inputFilePath)
        let headerLine = sr.ReadLine()
        let windowSize, inputPeriod, numFrames, initialThreshold, frameFrequencyThreshold = IOUtilities.ParseStringToTuple<int * int * int * float * float>(headerLine)
        let numPatterns = System.Int32.Parse(sr.ReadLine())

        let firingPatternMap =
            Seq.init numPatterns (fun index ->
                    let patternString = sr.ReadLine()
                    let pattern =
                        if patternString.StartsWith("None") then
                            None
                        else
                            Some(IOUtilities.ParseStringToList<int * int>(patternString))
                    index, pattern
                )
            |> Map.ofSeq

        let indexStack, patternProfilesList =
            // read the rest of the file and tokenise it
            seq {
                while (sr.Peek() >= 0) do
                    let line = sr.ReadLine()
                    let tokens = IOUtilities.Tokenize(List.ofArray (line.ToCharArray()))
                    yield tokens
            }
            |> Seq.fold (fun (indexStack:System.Collections.Generic.Stack<int>, patternProfilesList:int list list) tokens ->
                    // create a list of profiles where each profile is a list (includes paternIndex and neuronIndex)
                    if tokens.Length = 0 then               // start of new pattern
                        if indexStack.Count > 0 then
                            indexStack.Pop() |> ignore      // remove previous pattern index
                        indexStack, patternProfilesList
                    else
                        if tokens.Length = 1 then
                            // expecting either the pattern index or the neuron index
                            let index =
                                let items = IOUtilities.ConstructList<int> tokens
                                items.Head
                            indexStack.Push(index)
                            indexStack, patternProfilesList
                        else
                            // expecting the profile data
                            let spikeCounts = IOUtilities.ConstructTupleList<int> tokens
                            let neuronIndex = indexStack.Pop()
                            let patternIndex = indexStack.Peek()
                            let patternNeuronList = patternIndex :: neuronIndex :: spikeCounts
                            
                            indexStack, patternNeuronList :: patternProfilesList
                ) (new System.Collections.Generic.Stack<int>(), [])

        let frameProfilesByPattern =
            patternProfilesList
            |> Seq.fold (fun (outer:Map<int, Map<int, Map<int, int>>>) patternList ->
                    let patternIndex, neuronIndex, profile =
                        match patternList with
                        | patternIndex :: neuronIndex :: profile -> patternIndex, neuronIndex, profile
                        | [] -> 0, 0, []
                        | [_] -> 0, 0, []
                    
                    let profileMap =
                        profile
                        |> Seq.mapi (fun index count -> index, count)
                        |> Map.ofSeq

                    let inner =
                        if outer.ContainsKey patternIndex then
                            outer.[patternIndex]
                        else
                            Map.empty

                    Map.add patternIndex (Map.add neuronIndex profileMap inner) outer
                ) Map.empty

        if verbose then
            printfn "done"

        new PatternStatistics(windowSize, inputPeriod, numFrames, initialThreshold, frameFrequencyThreshold, firingPatternMap, frameProfilesByPattern)

    // --------------------------------------------------------------------------------------------------
    // Conversion Methods

    member this.PatternsAsMap = firingPatternMap

    member this.PatternsAsList =
        firingPatternMap
        |> Map.toSeq
        |> Seq.map (fun (patternIndex, pattern) -> pattern)
        |> Seq.toList

    member this.PatternsAsPatternList(includeNullPattern) =
        let patternList =
            firingPatternMap
            |> Map.toSeq
            |> Seq.map (fun (index, pattern) -> pattern)
            |> Seq.filter (fun pattern -> includeNullPattern || pattern.IsSome)
            |> Seq.toList
        new PatternList(patternList)

    member this.PatternNeuronWindowDictionary = ConvertToExternalMap this.PatternNeuronWindowMap

    member this.NeuronPatternWindowDictionary = ConvertToExternalMap this.NeuronPatternWindowMap

    member this.PatternSpikeFrequencyDictionary = ConvertToExternalSpikeFrequencyMap this.PatternSpikeFrequencyMap

    //member this.PatternFrequencyDictionary = ConvertMapToDictionary this.PatternFrequencyMap

    member this.PatternSpikeDistributionDictionary = ConvertToExternalSpikeDistributionMap this.PatternSpikeDistributionMap
